/* NOTICE
 
    Jim - the Java Image Manager, copyright (c) 2005 - 2007 Grant Gardner 
 
    Jim is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Jim is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  
    In addition, as a special exception, you are permitted to link this code
    with the 3rd party libraries required to compile this file and to
    distribute a linked combination including them. You must obey the
    GNU General Public License in all respects for all of the code except for
    those 3rd party libraries that are licenced under terms incompatible with the
    GNU General Public License.  
 
 
NOTICE */

/*
 * Created on 13/12/2004
 *
 *This material from The Java(tm) Specialists' Newsletter by Maximum Solutions (South Africa).
 *Please contact Maximum Solutions  for more information.
 *http://www.javaspecialists.co.za/archive/Issue082.html
 */

package au.com.lastweekend.jim.swing;

import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.ButtonGroup;
import javax.swing.ButtonModel;
import javax.swing.Icon;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.ActionMapUIResource;

import au.com.lastweekend.jim.util.TriState;

/**
 * Maintenance tip - There were some tricks to getting this code working:
 * 
 * 1. You have to overwite addMouseListener() to do nothing 2. You have to add a mouse event on mousePressed by calling
 * super.addMouseListener() 3. You have to replace the UIActionMap for the keyboard event "pressed" with your own one. 4. You have
 * to remove the UIActionMap for the keyboard event "released". 5. You have to grab focus when the next state is entered, otherwise
 * clicking on the component won't get the focus. 6. You have to make a TristateDecorator as a button model that wraps the original
 * button model and does state management.
 * 
 * @version $Id: TriStateCheckBox.java,v 1.4 2006/02/19 06:04:55 ggardner Exp $
 */


@SuppressWarnings("serial")
public class TriStateCheckBox extends JCheckBox {

    private TristateDecorator model = null;

    @SuppressWarnings("serial")
    public TriStateCheckBox(String text, Icon icon, TriState initial) {

        super(text, icon);
        // Add a listener for when the mouse is pressed
        super.addMouseListener(new MouseAdapter() {

            @Override
            public void mousePressed(MouseEvent e) {

                grabFocus();
                model.setState(getState().nextState());
            }
        });
        // Reset the keyboard action map
        ActionMap map = new ActionMapUIResource();
        map.put("pressed", new AbstractAction() {

            public void actionPerformed(ActionEvent e) {

                grabFocus();
                model.setState(getState().nextState());
            }
        });
        map.put("released", null);
        SwingUtilities.replaceUIActionMap(this, map);
        // set the model to the adapted model
        model = new TristateDecorator(getModel());
        setModel(model);
        setState(initial);
    }

    public TriStateCheckBox(String text, TriState initial) {

        this(text, null, initial);
    }

    public TriStateCheckBox(String text) {

        this(text, TriState.UNKNOWN);
    }

    public TriStateCheckBox() {

        this(null);
    }

    /** No one may add mouse listeners, not even Swing! */
    @Override
    public void addMouseListener(MouseListener l) {

    }

    /**
     * Set the new state to either SELECTED, NOT_SELECTED or DONT_CARE. If state == null, it is treated as DONT_CARE.
     * 
     * @param state
     */
    public final void setState(TriState state) {

        model.setState(state);
    }

    /**
     * @return the current state, which is determined by the selection status of the model.
     */
    public TriState getState() {

        return model.getState();
    }

    @Override
    public void setSelected(boolean b) {

        if (b) {
            setState(TriState.SELECTED);
        } else {
            setState(TriState.NOT_SELECTED);
        }
    }

    /**
     * Exactly which Design Pattern is this? Is it an Adapter, a Proxy or a Decorator? In this case, my vote lies with the
     * Decorator, because we are extending functionality and "decorating" the original model with a more powerful model.
     */
    private final class TristateDecorator implements ButtonModel {

        private final ButtonModel other;

        private TristateDecorator(ButtonModel otherModel) {

            this.other = otherModel;
        }

        private void setState(TriState state) {

            if (state == TriState.NOT_SELECTED) {
                other.setArmed(false);
                other.setRollover(false);
                setPressed(false);
                setSelected(false);
            } else if (state == TriState.SELECTED) {
                other.setArmed(false);
                other.setRollover(false);
                setPressed(false);
                setSelected(true);
            } else { // either "null" or DONT_CARE
                other.setArmed(false);
                other.setRollover(true);
                setPressed(true);
                setSelected(true);
            }
        }

        /**
         * The current state is embedded in the selection / armed state of the model.
         * 
         * We return the SELECTED state when the checkbox is selected but not armed, DONT_CARE state when the checkbox is selected
         * and armed (grey) and NOT_SELECTED when the checkbox is deselected.
         */
        private TriState getState() {

            if (isSelected() && !isRollover()) {
                // normal black tick
                return TriState.SELECTED;
            } else if (isSelected() && isRollover()) {
                // don't care grey tick
                return TriState.UNKNOWN;
            } else {
                // normal deselected
                return TriState.NOT_SELECTED;
            }
        }

        /** Filter: No one may change the armed or rollover status except us. */
        public void setArmed(boolean b) {

        }

        public void setRollover(boolean b) {

        }

        /**
         * We disable focusing on the component when it is not enabled.
         */
        public void setEnabled(boolean b) {

            setFocusable(b);
            other.setEnabled(b);
        }

        public boolean isRollover() {

            return other.isRollover();
        }

        /**
         * All these methods simply delegate to the "other" model that is being decorated.
         */
        public boolean isArmed() {

            return other.isArmed();
        }

        public boolean isSelected() {

            return other.isSelected();
        }

        public boolean isEnabled() {

            return other.isEnabled();
        }

        public boolean isPressed() {

            return other.isPressed();
        }

        public void setSelected(boolean b) {

            other.setSelected(b);
        }

        public void setPressed(boolean b) {

            other.setPressed(b);
        }

        public void setMnemonic(int key) {

            other.setMnemonic(key);
        }

        public int getMnemonic() {

            return other.getMnemonic();
        }

        public void setActionCommand(String s) {

            other.setActionCommand(s);
        }

        public String getActionCommand() {

            return other.getActionCommand();
        }

        public void setGroup(ButtonGroup group) {

            other.setGroup(group);
        }

        public void addActionListener(ActionListener l) {

            other.addActionListener(l);
        }

        public void removeActionListener(ActionListener l) {

            other.removeActionListener(l);
        }

        public void addItemListener(ItemListener l) {

            other.addItemListener(l);
        }

        public void removeItemListener(ItemListener l) {

            other.removeItemListener(l);
        }

        public void addChangeListener(ChangeListener l) {

            other.addChangeListener(l);
        }

        public void removeChangeListener(ChangeListener l) {

            other.removeChangeListener(l);
        }

        public Object[] getSelectedObjects() {

            return other.getSelectedObjects();
        }
    }

    public static void main(String[] args) throws Exception {

        JFrame frame = new JFrame("TristateCheckBoxTest");
        frame.getContentPane().setLayout(new GridLayout(0, 1, 5, 5));
        final TriStateCheckBox swingBox = new TriStateCheckBox("Testing the tristate checkbox");
        swingBox.setMnemonic('T');
        frame.getContentPane().add(swingBox);
        frame.getContentPane().add(new JCheckBox("The normal checkbox"));
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        final TriStateCheckBox winBox = new TriStateCheckBox("Testing the tristate checkbox", TriState.SELECTED);
        frame.getContentPane().add(winBox);
        final JCheckBox winNormal = new JCheckBox("The normal checkbox");
        frame.getContentPane().add(winNormal);
        // wait for 3 seconds, then enable all check boxes
        new Thread() {

            {
                start();
            }

            @Override
            public void run() {

                try {
                    winBox.setEnabled(false);
                    winNormal.setEnabled(false);
                    Thread.sleep(3000);
                    winBox.setEnabled(true);
                    winNormal.setEnabled(true);
                } catch (InterruptedException ex) {
                    // do nothing
                }
            }
        };
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }
}
